//@version=6
indicator("Dynamic Market Structure (MTF) - Dow Theory", "MSR", overlay=true, max_labels_count=500, max_lines_count=500)

//==============================================================================
//== SETTINGS ==================================================================
//==============================================================================
g_general = "General Settings"
pivotLookbackLeft = input.int(15, title="Pivot Lookback Left", tooltip="Number of bars to the left to look for a pivot point.", group=g_general)
pivotLookbackRight = input.int(5, title="Pivot Lookback Right", tooltip="Number of bars to the right for pivot confirmation. Recommended: 1 for Weekly, 5 for other timeframes.", group=g_general)
showBOS = input.bool(true, title="Show Break of Structure (BOS)", group=g_general)
showCHoCH = input.bool(true, title="Show Change of Character (CHoCH)", group=g_general)
maxLines = input.int(25, title="Max Structure Lines", tooltip="The maximum number of BOS/CHoCH lines to show.", group=g_general)
maxLabels = input.int(50, title="Max All Labels", tooltip="The maximum number of ALL labels (HH, LL, BOS, etc.) to show.", group=g_general)

g_mtf = "Multi-Timeframe Confirmation"
useMtfConfirmation = input.bool(true, title="Use MTF Confirmation", tooltip="If enabled, the script will look at a lower timeframe to confirm swing points, labeling them as 'Firm' or 'Soft'.", group=g_mtf)
confTimeframe = input.timeframe("240", title="Confirmation Timeframe", tooltip="The lower timeframe to use for confirming HL/LH pivots. Should be lower than your current chart timeframe.", group=g_mtf)
confLookback = input.int(5, title="Confirmation Lookback", tooltip="The pivot lookback period to use on the lower timeframe for detecting trend reversals.", group=g_mtf)

g_style = "Style Settings"
bullishColor = input.color(color.green, "Bullish Color", group=g_style)
bearishColor = input.color(color.red, "Bearish Color", group=g_style)
chochColor = input.color(color.orange, "CHoCH Color", group=g_style)
candleTransparency = input.int(30, title="Candle Transparency", minval=0, maxval=100, tooltip="Set the transparency for the trend-based candle colors. 0 is fully opaque, 100 is invisible.", group=g_style)
showInvalidation = input.bool(true, "Show Invalidation Levels", tooltip="If enabled, plots a dotted line at the pivot that, if broken, would invalidate the prior structure break.", group=g_style)


//==============================================================================
//== DATA STORAGE & PIVOT DETECTION ============================================
//==============================================================================

// Arrays to store the history of pivot points
var ph_p = array.new_float(0) // Pivot High Prices
var ph_b = array.new_int(0)   // Pivot High Bars
var pl_p = array.new_float(0) // Pivot Low Prices
var pl_b = array.new_int(0)   // Pivot Low Bars

// Arrays to manage ALL drawn lines and labels for cleanup
var line_array = array.new_line()
var label_array = array.new_label()
// Map to ensure pivots are only processed once, preventing repaint/scaling bugs.
var map<int, bool> processed_pivots = map.new<int, bool>()

// Trend state: 1 for Bullish, -1 for Bearish (for candle coloring)
var trend = 0

// Pine Script's built-in functions to detect swing high and low points
float pivotHighPrice = ta.pivothigh(high, pivotLookbackLeft, pivotLookbackRight)
float pivotLowPrice = ta.pivotlow(low, pivotLookbackLeft, pivotLookbackRight)
int pivotBar = bar_index - pivotLookbackRight

//==============================================================================
//== MULTI-TIMEFRAME LOGIC (MOVED TO GLOBAL SCOPE FOR STABILITY) ===============
//==============================================================================

// For a Firm LH, the LTF must have just reversed down by making a Lower Low.
ltf_pl_expr = ta.pivotlow(low, confLookback, confLookback)
ltf_reversal_down = na(ltf_pl_expr) ? false : ltf_pl_expr < ltf_pl_expr[1]
is_confirmed_down = request.security(syminfo.tickerid, confTimeframe, ltf_reversal_down[confLookback])

// For a Firm HL, the LTF must have just reversed up by making a Higher High.
ltf_ph_expr = ta.pivothigh(high, confLookback, confLookback)
ltf_reversal_up = na(ltf_ph_expr) ? false : ltf_ph_expr > ltf_ph_expr[1]
is_confirmed_up = request.security(syminfo.tickerid, confTimeframe, ltf_reversal_up[confLookback])


//==============================================================================
//== HELPER FUNCTIONS FOR DRAWING ==============================================
//==============================================================================

f_draw_structure_line(start_bar, end_bar, price, p_text, line_color) =>
    string label_text = p_text + ": " + str.tostring(price, format.mintick)
    line new_line = line.new(start_bar, price, end_bar, price, color=line_color, style=line.style_dashed, width=1)
    
    // Position label at the start of the line (left side)
    label new_label = label.new(start_bar, price, label_text, color=color.new(line_color, 80), textcolor=color.white, style=label.style_label_right, size=size.small, yloc=yloc.price)
    
    line_array.push(new_line)
    label_array.push(new_label)

f_draw_invalidation_line(start_bar, end_bar, price, line_color) =>
    line new_line = line.new(start_bar, price, end_bar, price, color=line_color, style=line.style_dotted, width=1)
    
    // Position label at the start of the line (left side)
    label new_label = label.new(start_bar, price, "INV", color=color.new(line_color, 80), textcolor=color.white, style=label.style_label_right, size=size.tiny, yloc=yloc.price)
    
    line_array.push(new_line)
    label_array.push(new_label)

//==============================================================================
//== CORE "DRAW-ONCE" MARKET STRUCTURE LOGIC (FLEXIBLE VERSION) ================
//==============================================================================

// --- Manage Pivot Highs ---
if not na(pivotHighPrice)
    if not processed_pivots.contains(pivotBar)
        processed_pivots.put(pivotBar, true)
        
        float last_ph = array.size(ph_p) > 0 ? array.get(ph_p, 0) : na
        int last_ph_bar = array.size(ph_b) > 0 ? array.get(ph_b, 0) : na

        array.unshift(ph_p, pivotHighPrice)
        array.unshift(ph_b, pivotBar)

        if not na(last_ph)
            if pivotHighPrice > last_ph // Higher High
                bool was_bearish = array.size(pl_b) > 1 and array.get(pl_b, 0) > last_ph_bar
                if was_bearish and showCHoCH
                    f_draw_structure_line(last_ph_bar, pivotBar, last_ph, "CHoCH", chochColor)
                else if showBOS
                    f_draw_structure_line(last_ph_bar, pivotBar, last_ph, "BOS", bullishColor)
                
                // Draw the invalidation level, which is the last pivot low
                if showInvalidation and array.size(pl_p) > 0
                    f_draw_invalidation_line(array.get(pl_b, 0), pivotBar, array.get(pl_p, 0), bullishColor)

                trend := 1 // Set trend for coloring
                label hh_label = label.new(pivotBar, pivotHighPrice, "HH", style=label.style_label_down, color=color.new(color.black, 100), textcolor=bullishColor, yloc=yloc.price)
                label_array.push(hh_label)

            else // Lower High
                string lh_text = "LH"
                if useMtfConfirmation
                    lh_text := is_confirmed_down ? "FLH" : "SLH"
                
                label lh_label = label.new(pivotBar, pivotHighPrice, lh_text, style=label.style_label_down, color=color.new(color.black, 100), textcolor=bearishColor, yloc=yloc.price)
                label_array.push(lh_label)

// --- Manage Pivot Lows ---
if not na(pivotLowPrice)
    if not processed_pivots.contains(pivotBar)
        processed_pivots.put(pivotBar, true)
        
        float last_pl = array.size(pl_p) > 0 ? array.get(pl_p, 0) : na
        int last_pl_bar = array.size(pl_b) > 0 ? array.get(pl_b, 0) : na
        
        array.unshift(pl_p, pivotLowPrice)
        array.unshift(pl_b, pivotBar)

        if not na(last_pl)
            if pivotLowPrice < last_pl // Lower Low
                bool was_bullish = array.size(ph_b) > 1 and array.get(ph_b, 0) > last_pl_bar
                if was_bullish and showCHoCH
                    f_draw_structure_line(last_pl_bar, pivotBar, last_pl, "CHoCH", chochColor)
                else if showBOS
                    f_draw_structure_line(last_pl_bar, pivotBar, last_pl, "BOS", bearishColor)

                // Draw the invalidation level, which is the last pivot high
                if showInvalidation and array.size(ph_p) > 0
                    f_draw_invalidation_line(array.get(ph_b, 0), pivotBar, array.get(ph_p, 0), bearishColor)
                
                trend := -1 // Set trend for coloring
                label ll_label = label.new(pivotBar, pivotLowPrice, "LL", style=label.style_label_up, color=color.new(color.black, 100), textcolor=bearishColor, yloc=yloc.price)
                label_array.push(ll_label)
            
            else // Higher Low
                string hl_text = "HL"
                if useMtfConfirmation
                    hl_text := is_confirmed_up ? "FHL" : "SHL"

                label hl_label = label.new(pivotBar, pivotLowPrice, hl_text, style=label.style_label_up, color=color.new(color.black, 100), textcolor=bullishColor, yloc=yloc.price)
                label_array.push(hl_label)

//==============================================================================
//== CLEANUP LOGIC =============================================================
//==============================================================================

// Clean up old lines if the array exceeds the maximum size
while line_array.size() > maxLines
    line.delete(line_array.shift())

// Clean up old labels if the array exceeds the maximum size
while label_array.size() > maxLabels
    label.delete(label_array.shift())

//==============================================================================
//== VISUALS ===================================================================
//==============================================================================

// Color candles based on the determined trend
var color trendColor = na
if trend == 1
    trendColor := color.new(bullishColor, candleTransparency)
else if trend == -1
    trendColor := color.new(bearishColor, candleTransparency)

barcolor(trendColor, title="Trend Color")

